/**
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 10/04/2013
 *
 */
package br.com.vfcfw.commons.util;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.commons.lang.time.DateUtils;
import org.apache.log4j.Logger;

/**
 * Classe utilitária para manipulação de data e hora.
 * 
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 10/04/2013
 * 
 */
public class DataUtil extends DateUtils {

    private static final Logger LOGGER = Logger.getLogger(DataUtil.class);

    /** Formato de data brasileira (dd/MM/yyyy). */
    public static final String FORMATO_DATA_BR = "dd/MM/yyyy";

    /** Formato de data norte americana (yyyy/MM/dd). */
    public static final String FORMATO_DATA_US = "yyyy/MM/dd";

    /** Formato de hora norte americana (HH:mm:ss). */
    public static final String FORMATO_HORA_BR = "HH:mm:ss";

    /** Formato de hora norte americana (HH:mm:ss). */
    public static final String FORMATO_HORA_US = "HH:mm:ss";

    /** Formato de data e hora norte americana (dd/MM/yyyy HH:mm:ss). */
    public static final String FORMATO_DATA_HORA_BR = "dd/MM/yyyy HH:mm:ss";

    /** Formato de data e hora norte americana (yyyy/MM/dd HH:mm:ss). */
    public static final String FORMATO_DATA_HORA_US = "yyyy/MM/dd HH:mm:ss";

    /**
     * Retorna a data data com a hora, minuto, segundo e milisegundos configurados para zero.
     * 
     * @param data {@link Date} Data.
     * @return Data zerada.
     */
    public static Date zerarData(Date data) {

        if (data == null) {

            LogUtil.error(LOGGER, "Erro ao zerar data. Objeto data nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Zerando a hora, minuto, segundo e milisegundos da data.");

        Calendar calendar = new GregorianCalendar();
        calendar.setTime(data);

        calendar.set(Calendar.HOUR, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        calendar.set(Calendar.HOUR_OF_DAY, 0);

        return calendar.getTime();
    }

    /**
     * Retorna a data do dia anterior à data atual.
     * 
     * @return {@link Date} dia anterior.
     */
    public static Date obterDiaAnterior() {

        return obterDiaAnterior(new Date());
    }

    /**
     * Retorna a data do dia anterior à data informada.
     * 
     * @param data Data de referência.
     * @return {@link Date} dia anterior.
     */
    public static Date obterDiaAnterior(Date data) {

        if (data == null) {

            LogUtil.error(LOGGER, "Erro ao obter o dia anterior. Objeto data nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Obtendo o dia anterior da data.");

        Calendar hoje = new GregorianCalendar();
        hoje.setTime(zerarData(data));

        hoje.add(Calendar.DATE, -1);

        return hoje.getTime();
    }

    /**
     * Retorna a data do dia posterior à data atual.
     * 
     * @return {@link Date} dia posterior.
     */
    public static Date obterDiaPosterior() {

        return obterDiaPosterior(new Date());
    }

    /**
     * Retorna a data do dia posterior à data informada.
     * 
     * @param data Data de referência.
     * @return {@link Date} dia posterior.
     */
    public static Date obterDiaPosterior(Date data) {

        if (data == null) {

            LogUtil.error(LOGGER, "Erro ao obter o dia posterior. Objeto data nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Obtendo o dia posterior da data.");

        Calendar hoje = new GregorianCalendar();
        hoje.setTime(zerarData(data));

        hoje.add(Calendar.DATE, 1);

        return hoje.getTime();
    }

    /**
     * Retorna uma lista de datas dentro do período informado.
     * 
     * @param dataInicial objeto {@link Date} representando a data inicial.
     * @param dataFinal objeto {@link Date} representando a data final.
     * @return Lista com as datas.
     */
    public static List<Date> obterDiasPeriodo(Date dataInicial, Date dataFinal) {

        if (dataInicial == null || dataFinal == null) {

            LogUtil.error(LOGGER, "Erro ao obter o dias do período. Objeto data inicial ou data final nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Obtendo os dias do período.");

        List<Date> dias = new ArrayList<Date>();

        if (dataInicialMenorDataFinal(dataInicial, dataFinal)) {

            Date atual = dataInicial;

            do {

                dias.add(atual);

                atual = obterDiaPosterior(atual);

            } while (dataInicialMenorIgualDataFinal(atual, dataFinal));
        }
        return dias;
    }

    /**
     * Verifica se a data informada é um final de semana.
     * 
     * @param data {@link Date} Data.
     * @return <tt>TRUE</tt> se for final de semana, <tt>FALSE</tt> caso contrário.
     */
    public static boolean isFinalSemana(Date data) {

        if (data == null) {

            LogUtil.error(LOGGER, "Erro ao verificar se a data está em um final de semana. Objeto data nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Verificando se a data está em um final de semana.");

        Calendar calendar = dateParaCalendar(data);
        int diaSemana = calendar.get(7);

        return (diaSemana == 7) || (diaSemana == 1);
    }

    /**
     * Verifica se a data informada é um sábado.
     * 
     * @param data {@link Date} Data.
     * @return <tt>TRUE</tt> se for sábado, <tt>FALSE</tt> caso contrário.
     */
    public static boolean isSabado(Date data) {

        if (data == null) {

            LogUtil.error(LOGGER, "Erro ao verificar se a data está em um sábado. Objeto data nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Verificando se a data está em um sábado.");

        return (dateParaCalendar(data).get(7) == 7);
    }

    /**
     * Verifica se a data informada é um domingo.
     * 
     * @param data {@link Date} Data.
     * @return <tt>TRUE</tt> se for domingo, <tt>FALSE</tt> caso contrário.
     */
    public static boolean isDomingo(Date data) {

        if (data == null) {

            LogUtil.error(LOGGER, "Erro ao verificar se a data está em um domingo. Objeto data nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Verificando se a data está em um domingo.");

        return (dateParaCalendar(data).get(7) == 1);
    }

    /**
     * Converte uma data de {@link Date} para {@link java.sql.Date}.
     * 
     * @param data Data no formato {@link Date}.
     * @return {@link java.sql.Date}.
     */
    public static java.sql.Date dateParaSqlDate(Date data) {

        if (data == null) {

            LogUtil.error(LOGGER, "Erro ao converter a data de java.util.Date para java.sql.Date. Objeto data nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Convertendo a data de java.util.Date para java.sql.Date.");

        return new java.sql.Date(data.getTime());
    }

    /**
     * Converte uma data de {@link Date} para {@link Calendar}.
     * 
     * @param data Data no formato {@link Date}.
     * @return {@link Calendar}.
     */
    public static Calendar dateParaCalendar(Date data) {

        if (data == null) {

            LogUtil.error(LOGGER,
                "Erro ao converter a data de java.util.Date para java.util.Calendar. Objeto data nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Convertendo a data de java.util.Date para java.util.Calendar.");

        Calendar c = Calendar.getInstance();
        c.setTime(data);
        return c;
    }

    /**
     * Converte uma data de {@link Date} para {@link XMLGregorianCalendar}. No formato: yyyy-MM-ddTHH:mm:ss.SSS.
     * 
     * @param data {@link Date} Data.
     * @return Data em {@link XMLGregorianCalendar}.
     * @since 02/10/2012
     */
    public static XMLGregorianCalendar dateParaXMLGregorianCalendar(Date data) {

        return dateParaXMLGregorianCalendar(data, true, false);
    }

    /**
     * Converte a data no formato {@link Date} para o formato texto no padrão XMLGregorianCalendar.
     * 
     * @param data {@link Date}.
     * @param timezone <tt>TRUE</tt> exibe o timezone na data convertida, <tt>FALSE</tt> não exibe o timezone.
     * @param milisegundos <tt>TRUE</tt> exibe os milisegundos na data convertida, <tt>FALSE</tt> não exibe os
     *        milisegundos.
     * @return Data no formato XMLGregorianCalendar.
     */
    public static XMLGregorianCalendar dateParaXMLGregorianCalendar(Date data, boolean timezone, boolean milisegundos) {

        if (data == null) {

            LogUtil
                .error(LOGGER,
                    "Erro ao converter a data de java.util.Date para javax.xml.datatype.XMLGregorianCalendar. Objeto data nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Convertendo a data de java.util.Date para javax.xml.datatype.XMLGregorianCalendar.");

        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(data);
        XMLGregorianCalendar xmlGregorianCalendar;

        try {

            xmlGregorianCalendar = DatatypeFactory.newInstance().newXMLGregorianCalendar(calendar);

            if (!timezone) {
                xmlGregorianCalendar.setTimezone(DatatypeConstants.FIELD_UNDEFINED);
            }

            if (!milisegundos) {
                xmlGregorianCalendar.setMillisecond(DatatypeConstants.FIELD_UNDEFINED);
            }

        } catch (DatatypeConfigurationException e) {
            xmlGregorianCalendar = null;
        }
        return xmlGregorianCalendar;
    }

    /**
     * Converte a data no formato {@link XMLGregorianCalendar} para o formato {@link Date}.
     * 
     * @param xmlGregorianCalendar {@link XMLGregorianCalendar}.
     * @return {@link Date}.
     */
    public static Date xmlGregorianCalendarParaDate(XMLGregorianCalendar xmlGregorianCalendar) {

        if (xmlGregorianCalendar == null) {

            LogUtil
                .error(
                    LOGGER,
                    "Erro ao converter a data de javax.xml.datatype.XMLGregorianCalendar para java.util.Date. Objeto xmlGregorianCalendar nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Convertendo a data de javax.xml.datatype.XMLGregorianCalendar para java.util.Date.");
        Date data = null;

        try {

            data = xmlGregorianCalendar.toGregorianCalendar().getTime();
        } catch (Exception e) {
            data = null;
        }
        return data;
    }

    /**
     * Retorna a diferença em dias entre duas datas.
     * 
     * @param dataInicial objeto {@link Date} representando a data inicial
     * @param dataFinal objeto {@link Date} representando a data final
     * @return Diferença em dias
     */
    public static double diferencaEmDias(Date dataInicial, Date dataFinal) {

        if (dataInicial == null || dataFinal == null) {

            LogUtil.error(LOGGER,
                "Erro ao obter a diferença em dias do período. Objeto data inicial ou data final nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Obtendo a diferença em dias do período.");

        double resultado = 0;
        long diferenca = dataFinal.getTime() - dataInicial.getTime();

        // Calcula a diferenca em dias
        double diferencaEmDias = (diferenca / 1000) / 60 / 60 / 24;

        // Calcula as horas restantes
        long horasRestantes = (diferenca / 1000) / 60 / 60 % 24;

        // Adiciona as horas restantes como fracao de um dia
        resultado = diferencaEmDias + horasRestantes;

        return resultado;
    }

    /**
     * Verifica se a data inicial é menor que a data final.
     * 
     * @param dataInicial {@link Date} Data inicial.
     * @param dataFinal {@link Date} Data final.
     * @return <tt>TRUE</tt> se a data inicial for menor que a data final, <tt>FALSE</tt> caso contrário.
     */
    public static boolean dataInicialMenorDataFinal(Date dataInicial, Date dataFinal) {

        if (dataInicial == null || dataFinal == null) {

            LogUtil
                .error(LOGGER,
                    "Erro ao obter ao verificar se a data inicial é menor que a data final. Objeto data inicial ou data final nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Verificando se a data inicial é menor que a data final.");

        return dataInicial.compareTo(dataFinal) < 0;
    }

    /**
     * Verifica se a data inicial é menor ou igual que a data final.
     * 
     * @param dataInicial {@link Date} Data inicial.
     * @param dataFinal {@link Date} Data final.
     * @return <tt>TRUE</tt> se a data inicial for menor ou igual que a data final, <tt>FALSE</tt> caso contrário.
     */
    public static boolean dataInicialMenorIgualDataFinal(Date dataInicial, Date dataFinal) {

        if (dataInicial == null || dataFinal == null) {

            LogUtil
                .error(
                    LOGGER,
                    "Erro ao obter ao verificar se a data inicial é menor ou igual que a data final. Objeto data inicial ou data final nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Verificando se a data inicial é menor ou igual que a data final.");

        return dataInicial.compareTo(dataFinal) <= 0;
    }

    /**
     * Verifica se a data inicial é maior que a data final.
     * 
     * @param dataInicial {@link Date} Data inicial.
     * @param dataFinal {@link Date} Data final.
     * @return <tt>TRUE</tt> se a data inicial for maior que a data final, <tt>FALSE</tt> caso contrário.
     */
    public static boolean dataInicialMaiorDataFinal(Date dataInicial, Date dataFinal) {

        if (dataInicial == null || dataFinal == null) {

            LogUtil
                .error(LOGGER,
                    "Erro ao obter ao verificar se a data inicial é maior que a data final. Objeto data inicial ou data final nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Verificando se a data inicial é maior que a data final.");

        return dataInicial.compareTo(dataFinal) > 0;
    }

    /**
     * Verifica se a data inicial é maior ou igual que a data final.
     * 
     * @param dataInicial {@link Date} Data inicial.
     * @param dataFinal {@link Date} Data final.
     * @return <tt>TRUE</tt> se a data inicial for maior que a data final, <tt>FALSE</tt> caso contrário.
     */
    public static boolean dataInicialMaiorIgualDataFinal(Date dataInicial, Date dataFinal) {

        if (dataInicial == null || dataFinal == null) {

            LogUtil
                .error(
                    LOGGER,
                    "Erro ao obter ao verificar se a data inicial é maior ou igual que a data final. Objeto data inicial ou data final nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Verificando se a data inicial é maior ou igual que a data final.");

        return dataInicial.compareTo(dataFinal) >= 0;
    }

    /**
     * Calcula a idade baseado na data de nascimento.
     * 
     * @param dataNascimento {@link Date} Data de nascimento.
     * @return Idade.
     */
    public static int calcularIdade(Date dataNascimento) {

        if (dataNascimento == null) {

            LogUtil.error(LOGGER, "Erro ao calcular a idade baseado na data atual. Objeto data de nascimento.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Calculando a idade baseado na data atual.");

        return calcularIdade(dataNascimento, new Date());
    }

    /**
     * Calcula a idade baseado na data de nascimento.
     * 
     * @param dataNascimento {@link Date} Data de nascimento.
     * @param dataReferencia {@link Date} Data de referência.
     * @return Idade.
     */
    public static int calcularIdade(Date dataNascimento, Date dataReferencia) {

        if (dataNascimento == null || dataReferencia == null) {

            LogUtil
                .error(LOGGER,
                    "Erro ao calcular a idade baseado na data de referência. Objeto data de nascimento ou data de referência nulo.");

            throw new IllegalArgumentException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_OBJETO_NULO));
        }

        LogUtil.debug(LOGGER, "Calculando a idade baseado na data de referência.");

        Calendar calendar = Calendar.getInstance();

        calendar.setTime(dataReferencia);
        int diaAnoAtual = calendar.get(Calendar.DAY_OF_YEAR);
        int anoAtual = calendar.get(Calendar.YEAR);

        calendar.setTime(dataNascimento);
        int diaAnoNascimento = calendar.get(Calendar.DAY_OF_YEAR);
        int anoNascimento = calendar.get(Calendar.YEAR);

        int idade = anoAtual - anoNascimento;

        if (diaAnoAtual < diaAnoNascimento) {

            idade--;
        }
        return idade;
    }

}
